﻿// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// Copyright (c) Microsoft Corporation. All rights reserved.

namespace Microsoft.Samples.Commerce.Order
{
    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.Commerce.Common.MessageBuilders;
    using Microsoft.Commerce.Contracts;
    using Microsoft.Commerce.Contracts.Messages;
    using Microsoft.Samples.Commerce.Order.CommerceEntities;

    /// <summary>
    /// This class demonstrates how to use the Commerce Foundation APIs to manage the shopping cart and to 
    /// implement the checkout process. The code in this class is not production ready and is 
    /// intended to be used as the Commerce Foundation Order APIs "How to".
    /// </summary>
    public static class BasketManager
    {
        /// <summary>
        /// Sample basket name.
        /// </summary>
        public const string BasketName = "Default";

        /// <summary>
        /// Retrieves the basket populated with the line items only.
        /// </summary>
        /// <returns>The Basket entity.</returns>
        public static Basket GetBasket()
        {
            return GetBasket(false, false, false);
        }

        /// <summary>
        /// Retrieves the basket populated with the line items, and optionally with the addresses,
        /// payments and shipments.
        /// </summary>
        /// <param name="includeAddresses">if set to <c>true</c> [include addresses].</param>
        /// <param name="includePayments">if set to <c>true</c> [include payments].</param>
        /// <param name="includeShipments">if set to <c>true</c> [include shipments].</param>
        /// <returns>The Basket entity.</returns>
        public static Basket GetBasket(bool includeAddresses, bool includePayments, bool includeShipments)
        {
            var basketQuery = new CommerceQuery<Basket>();

            // Specify the search criteria
            basketQuery.SearchCriteria.Model.UserId = CommerceFoundationServiceAgent.UserId;
            basketQuery.SearchCriteria.Model.Name = BasketName;
            basketQuery.SearchCriteria.Model.BasketType = BasketType.Cart;

            // Specify that line items to be returned
            AddLineItemRelatedQueries(basketQuery.RelatedOperations);

            // Specify that addresses to be returned if requested
            if (includeAddresses)
            {
                AddAddressRelatedQueries(basketQuery.RelatedOperations);
            }

            // Specify that payments to be returned if requested
            if (includePayments)
            {
                AddPaymentRelatedQueries(basketQuery.RelatedOperations);
            }

            // Specify that shipments to be returned if requested
            if (includeShipments)
            {
                AddShipmentRelatedQueries(basketQuery.RelatedOperations);
            }

            // Execute the operation and get the results back
            CommerceResponse response = CommerceFoundationServiceAgent.Execute(basketQuery);
            CommerceQueryOperationResponse queryResponse = response.OperationResponses.Single() as CommerceQueryOperationResponse;
            CommerceEntity basket = queryResponse.CommerceEntities.SingleOrDefault();
            return (basket != null) ? basket : null;
        }

        /// <summary>
        /// Deletes the basket
        /// </summary>
        public static void DeleteBasket()
        {
            var deleteBasket = new CommerceDelete<Basket>();

            // Specify the search criteria
            deleteBasket.SearchCriteria.Model.UserId = CommerceFoundationServiceAgent.UserId;
            deleteBasket.SearchCriteria.Model.Name = BasketName;
            deleteBasket.SearchCriteria.Model.BasketType = BasketType.Cart;

            // Execute the operation
            CommerceFoundationServiceAgent.Execute(deleteBasket);
        }

        /// <summary>
        /// Adds a new line item to the basket.
        /// </summary>
        /// <param name="lineItem">line item to add</param>
        /// <returns>The Basket entity.</returns>
        public static Basket AddLineItem(LineItem lineItem)
        {
            var basketUpdate = new CommerceUpdate<Basket>();

            // Specify the search criteria
            basketUpdate.SearchCriteria.Model.UserId = CommerceFoundationServiceAgent.UserId;
            basketUpdate.SearchCriteria.Model.Name = BasketName;
            basketUpdate.SearchCriteria.Model.BasketType = BasketType.Cart;

            // Specify the line item to be added
            var lineItemCreate = new CommerceCreateRelatedItem<LineItem>(Basket.RelationshipName.LineItems);
            lineItemCreate.Model = lineItem;
            basketUpdate.RelatedOperations.Add(lineItemCreate);

            // Specify the data to be returned on the completion of the operation
            basketUpdate.UpdateOptions.ReturnModel = new Basket();
            AddLineItemRelatedQueries(basketUpdate.UpdateOptions.ReturnModelQueries);

            // Execute the operation and get the results back
            CommerceResponse response = CommerceFoundationServiceAgent.Execute(basketUpdate);
            CommerceUpdateOperationResponse operationResponse = response.OperationResponses.Single() as CommerceUpdateOperationResponse;
            CommerceEntity basket = operationResponse.CommerceEntities.SingleOrDefault();
            return (basket != null) ? basket : null;
        }

        /// <summary>
        /// Updates quantity of a line item in the basket.
        /// </summary>
        /// <param name="lineItemId">line item to be updated</param>
        /// <param name="quantity">new quantity for the line item</param>
        /// <returns>The Basket entity.</returns>
        public static Basket UpdateLineItemQuantity(string lineItemId, decimal quantity)
        {
            var basketUpdate = new CommerceUpdate<Basket>();

            // Specify the search criteria
            basketUpdate.SearchCriteria.Model.UserId = CommerceFoundationServiceAgent.UserId;
            basketUpdate.SearchCriteria.Model.Name = BasketName;
            basketUpdate.SearchCriteria.Model.BasketType = BasketType.Cart;

            // Specify the line item to be updated
            var lineItemUpdate = new CommerceUpdateRelatedItem<LineItem>(Basket.RelationshipName.LineItems);
            lineItemUpdate.SearchCriteria.Model.Id = lineItemId;
            lineItemUpdate.Model.Quantity = quantity;
            basketUpdate.RelatedOperations.Add(lineItemUpdate);

            // Specify the data to be returned on the completion of the operation
            basketUpdate.UpdateOptions.ReturnModel = new Basket();
            AddLineItemRelatedQueries(basketUpdate.UpdateOptions.ReturnModelQueries);

            // Execute the operation and get the results back
            CommerceResponse response = CommerceFoundationServiceAgent.Execute(basketUpdate);
            CommerceUpdateOperationResponse operationResponse = response.OperationResponses.Single() as CommerceUpdateOperationResponse;
            CommerceEntity basket = operationResponse.CommerceEntities.SingleOrDefault();
            return (basket != null) ? basket : null;
        }

        /// <summary>
        /// Removes line item from the basket.
        /// </summary>
        /// <param name="lineItemId">id of the line item to be removed</param>
        /// <returns>The Basket entity.</returns>
        public static Basket RemoveLineItem(string lineItemId)
        {
            var basketUpdate = new CommerceUpdate<Basket>();

            // Specify the search criteria
            basketUpdate.SearchCriteria.Model.UserId = CommerceFoundationServiceAgent.UserId;
            basketUpdate.SearchCriteria.Model.Name = BasketName;
            basketUpdate.SearchCriteria.Model.BasketType = BasketType.Cart;

            // Specify the line item to be removed
            var lineItemDelete = new CommerceDeleteRelatedItem<LineItem>(Basket.RelationshipName.LineItems);
            lineItemDelete.SearchCriteria.Model.Id = lineItemId;
            basketUpdate.RelatedOperations.Add(lineItemDelete);

            // Specify the data to be returned on the completion of the operation
            basketUpdate.UpdateOptions.ReturnModel = new Basket();
            AddLineItemRelatedQueries(basketUpdate.UpdateOptions.ReturnModelQueries);

            // Execute the operation and get the results back
            CommerceResponse response = CommerceFoundationServiceAgent.Execute(basketUpdate);
            CommerceUpdateOperationResponse operationResponse = response.OperationResponses.Single() as CommerceUpdateOperationResponse;
            CommerceEntity basket = operationResponse.CommerceEntities.SingleOrDefault();
            return (basket != null) ? basket : null;
        }

        /// <summary>
        /// Prepares the basket for checkout. This method adds shipping and billing addresses, as well as
        /// shipping method and payment account information to the basket.
        /// </summary>
        /// <param name="shippingAddress">shipping address to add to the basket</param>
        /// <param name="billingAddress">billing address to add to the basket</param>
        /// <param name="paymentAccount">payment acccount to add to the basket</param>
        /// <param name="shippingMethodId">shipping method to add to the basket</param>
        /// <param name="lineItemIds">ids of the line items to be updated in the basket</param>
        /// <returns>The Basket entity.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "By Design.")]
        public static Basket PrepareForCheckout(
                                Address shippingAddress,
                                Address billingAddress,
                                PaymentAccount paymentAccount,
                                string shippingMethodId,
                                string[] lineItemIds, string paymentMethodId)
        {
            var basketUpdate = new CommerceUpdate<Basket>();

            // Specify the search criteria
            basketUpdate.SearchCriteria.Model.UserId = CommerceFoundationServiceAgent.UserId;
            basketUpdate.SearchCriteria.Model.Name = BasketName;
            basketUpdate.SearchCriteria.Model.BasketType = BasketType.Cart;

            // Before the basket can be converted from the cart to the order
            // its status should be set to ReadyForCheckout
            basketUpdate.Model.Status = BasketStatus.ReadyForCheckout;

            // Add shipping address to the basket
            var shippingAddressCreate = new CommerceCreateRelatedItem<Address>(Basket.RelationshipName.Addresses);
            shippingAddressCreate.Model = shippingAddress;
            basketUpdate.RelatedOperations.Add(shippingAddressCreate);

            // Add billing address to the basket
            var billingAddressCreate = new CommerceCreateRelatedItem<Address>(Basket.RelationshipName.Addresses);
            billingAddressCreate.Model = billingAddress;
            basketUpdate.RelatedOperations.Add(billingAddressCreate);

            //// Add payment to the basket
            var paymentCreate = new CommerceCreateRelatedItem<Payment>(Basket.RelationshipName.Payments);
            {
                // The payment must include a payment account
                var paymentAccountCreate = new CommerceCreateRelatedItem<PaymentAccount>(Payment.RelationshipName.PaymentAccount);
                {
                    paymentAccountCreate.Model = paymentAccount;

                    // The payment account must include a payment method
                    var paymentMethodAssign = new CommerceCreateRelationship<PaymentMethod>(PaymentAccount.RelationshipName.PaymentMethod);
                    {
                        paymentMethodAssign.SearchCriteria.Model.Id = paymentMethodId;
                        paymentAccountCreate.RelatedOperations.Add(paymentMethodAssign);
                    }

                    paymentCreate.RelatedOperations.Add(paymentAccountCreate);
                }

                basketUpdate.RelatedOperations.Add(paymentCreate);
            }

            // The basket's line items should be assoisiated with the corresponding shipping address and shipping method
            foreach (string lineItemId in lineItemIds)
            {
                var lineItemUpdate = new CommerceUpdateRelatedItem<LineItem>(Basket.RelationshipName.LineItems);
                lineItemUpdate.SearchCriteria.Model.Id = lineItemId;
                lineItemUpdate.Model.ShippingAddressId = shippingAddress.Id;
                lineItemUpdate.Model.ShippingMethodId = shippingMethodId;
                basketUpdate.RelatedOperations.Add(lineItemUpdate);
            }

            // Specify the data to be returned on the completion of the operation
            basketUpdate.UpdateOptions.ReturnModel = new Basket();
            AddLineItemRelatedQueries(basketUpdate.UpdateOptions.ReturnModelQueries);
            AddAddressRelatedQueries(basketUpdate.UpdateOptions.ReturnModelQueries);
            AddPaymentRelatedQueries(basketUpdate.UpdateOptions.ReturnModelQueries);
            AddShipmentRelatedQueries(basketUpdate.UpdateOptions.ReturnModelQueries);

            // Execute the operation and get the results back
            CommerceResponse response = CommerceFoundationServiceAgent.Execute(basketUpdate);
            CommerceUpdateOperationResponse operationResponse = response.OperationResponses.Single() as CommerceUpdateOperationResponse;
            CommerceEntity basket = operationResponse.CommerceEntities.SingleOrDefault();
            return (basket != null) ? basket : null;
        }

        /// <summary>
        /// Executes the checkout and converts the basket from the cart to the order.
        /// </summary>
        /// <returns>The Basket entity.</returns>
        public static Basket Checkout()
        {
            var basketUpdate = new CommerceUpdate<Basket>();

            // Specify the search criteria
            basketUpdate.SearchCriteria.Model.UserId = CommerceFoundationServiceAgent.UserId;
            basketUpdate.SearchCriteria.Model.Name = BasketName;
            basketUpdate.SearchCriteria.Model.BasketType = BasketType.Cart;

            // Checkout process converts the basket from the Cart type to the Order type
            basketUpdate.Model.BasketType = BasketType.Order;

            // Specify the data to be returned on the completion of the operation
            basketUpdate.UpdateOptions.ReturnModel = new Basket();
            AddLineItemRelatedQueries(basketUpdate.UpdateOptions.ReturnModelQueries);
            AddAddressRelatedQueries(basketUpdate.UpdateOptions.ReturnModelQueries);
            AddPaymentRelatedQueries(basketUpdate.UpdateOptions.ReturnModelQueries);
            AddShipmentRelatedQueries(basketUpdate.UpdateOptions.ReturnModelQueries);

            // Execute the operation and get the results back
            CommerceResponse response = CommerceFoundationServiceAgent.Execute(basketUpdate);
            CommerceUpdateOperationResponse operationResponse = response.OperationResponses.Single() as CommerceUpdateOperationResponse;
            CommerceEntity basket = operationResponse.CommerceEntities.SingleOrDefault();
            return (basket != null) ? basket : null;
        }

        /// <summary>
        /// Removes related items added to the basket during the checkout. 
        /// This method is used to clear the basket when the user cancels or 
        /// abandons the checkout process.
        /// </summary>
        /// <param name="basket">basket to clear</param>
        /// <returns>cleared basket</returns>
        public static Basket ClearBasket(Basket basket)
        {
            var basketUpdate = new CommerceUpdate<Basket>();

            // Specify the search criteria
            basketUpdate.SearchCriteria.Model.UserId = CommerceFoundationServiceAgent.UserId;
            basketUpdate.SearchCriteria.Model.Name = BasketName;
            basketUpdate.SearchCriteria.Model.BasketType = BasketType.Cart;

            // Basket's status should be InProgress
            basketUpdate.Model.Status = BasketStatus.InProcess;

            // Remove addresses
            if (basket.Addresses != null)
            {
                foreach (var relationship in basket.Addresses)
                {
                    var deleteAddress = new CommerceDeleteRelatedItem<Address>(Basket.RelationshipName.Addresses);
                    deleteAddress.SearchCriteria.Model.Id = relationship.Target.Id;
                    basketUpdate.RelatedOperations.Add(deleteAddress);
                }
            }

            // Remove payments
            if (basket.Payments != null)
            {
                foreach (var relationship in basket.Payments)
                {
                    var deletePayment = new CommerceDeleteRelatedItem<Payment>(Basket.RelationshipName.Payments);
                    deletePayment.SearchCriteria.Model.Id = relationship.Target.Id;
                    basketUpdate.RelatedOperations.Add(deletePayment);
                }
            }

            // Specify the data to be returned on the completion of the operation
            basketUpdate.Model.BasketType = BasketType.Order;
            basketUpdate.UpdateOptions.ReturnModel = new Basket();
            AddLineItemRelatedQueries(basketUpdate.UpdateOptions.ReturnModelQueries);
            AddAddressRelatedQueries(basketUpdate.UpdateOptions.ReturnModelQueries);
            AddPaymentRelatedQueries(basketUpdate.UpdateOptions.ReturnModelQueries);
            AddShipmentRelatedQueries(basketUpdate.UpdateOptions.ReturnModelQueries);

            // Execute the operation and get the results back
            CommerceResponse response = CommerceFoundationServiceAgent.Execute(basketUpdate);
            CommerceUpdateOperationResponse operationResponse = response.OperationResponses.Single() as CommerceUpdateOperationResponse;
            CommerceEntity updatedBasket = operationResponse.CommerceEntities.SingleOrDefault();
            return (updatedBasket != null) ? updatedBasket : null;
        }

        /// <summary>
        /// Returns a list of shipping methods configured for the commerce site.
        /// </summary>
        /// <returns>List of shipping methods.</returns>
        public static IEnumerable<ShippingMethod> LoadShippingMethods()
        {
            var query = new CommerceQuery<ShippingMethod>();

            CommerceResponse response = CommerceFoundationServiceAgent.Execute(query);
            CommerceQueryOperationResponse queryResponse = response.OperationResponses.Single() as CommerceQueryOperationResponse;

            if (queryResponse != null && queryResponse.CommerceEntities != null)
            {
                return 
                    from entity in queryResponse.CommerceEntities 
                    select new ShippingMethod(entity);
            }
            else
            {
                return new List<ShippingMethod>();
            }
        }

        /// <summary>
        /// Returns a list of payment methods configured for the commerce site.
        /// </summary>
        /// <returns>List of payment methods.</returns>
        public static IEnumerable<PaymentMethod> LoadPaymentMethods()
        {
            var query = new CommerceQuery<PaymentMethod>();

            CommerceResponse response = CommerceFoundationServiceAgent.Execute(query);
            CommerceQueryOperationResponse queryResponse = response.OperationResponses.Single() as CommerceQueryOperationResponse;

            if (queryResponse != null && queryResponse.CommerceEntities != null)
            {
                return
                    from entity in queryResponse.CommerceEntities
                    select new PaymentMethod(entity);
            }
            else
            {
                return new List<PaymentMethod>();
            }
        }

        #region Private Helper Methods

        /// <summary>
        /// Adds the line item related queries.
        /// </summary>
        /// <param name="relatedOperations">The related operations.</param>
        private static void AddLineItemRelatedQueries(List<CommerceBaseRelatedOperation> relatedOperations)
        {
            var lineItemQuery = new CommerceQueryRelatedItem<LineItem>(Basket.RelationshipName.LineItems);
            var discountQuery = new CommerceQueryRelatedItem<Discount>(LineItem.RelationshipName.ItemLevelDiscounts);
            lineItemQuery.RelatedOperations.Add(discountQuery);
            relatedOperations.Add(lineItemQuery);
        }

        /// <summary>
        /// Adds the address related queries.
        /// </summary>
        /// <param name="relatedOperations">The related operations.</param>
        private static void AddAddressRelatedQueries(List<CommerceBaseRelatedOperation> relatedOperations)
        {
            var addressQuery = new CommerceQueryRelatedItem<Address>(Basket.RelationshipName.Addresses);
            relatedOperations.Add(addressQuery);
        }

        /// <summary>
        /// Adds the payment related queries.
        /// </summary>
        /// <param name="relatedOperations">The related operations.</param>
        private static void AddPaymentRelatedQueries(List<CommerceBaseRelatedOperation> relatedOperations)
        {
            var paymentQuery = new CommerceQueryRelatedItem<Payment>(Basket.RelationshipName.Payments);
            var paymentAccountQuery = new CommerceQueryRelatedItem<PaymentAccount>(Payment.RelationshipName.PaymentAccount);
            paymentQuery.RelatedOperations.Add(paymentAccountQuery);
            var paymentMethodQuery = new CommerceQueryRelatedItem<PaymentMethod>(PaymentAccount.RelationshipName.PaymentMethod);
            paymentAccountQuery.RelatedOperations.Add(paymentMethodQuery);

            relatedOperations.Add(paymentQuery);
        }

        /// <summary>
        /// Adds the shipment related queries.
        /// </summary>
        /// <param name="relatedOperations">The related operations.</param>
        private static void AddShipmentRelatedQueries(List<CommerceBaseRelatedOperation> relatedOperations)
        {
            var shipmentQuery = new CommerceQueryRelatedItem<Shipment>(Basket.RelationshipName.Shipments);
            var shippingMethodQuery = new CommerceQueryRelatedItem<ShippingMethod>(Shipment.RelationshipName.ShippingMethod);
            shipmentQuery.RelatedOperations.Add(shippingMethodQuery);
            relatedOperations.Add(shipmentQuery);
        }

        #endregion
    }
}
